// Copyright 2011 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview Classes used to support Message's in ProtoRpc.
 * @author joetyson@gmail.com (Joe Tyson)
 */

goog.provide('protorpc.Enum');
goog.provide('protorpc.EnumValue');
goog.provide('protorpc.Message');
goog.provide('protorpc.Variant');


goog.require('protorpc.Util.Error');



/**
 * Invalid value for message error.
 * @constructor
 * @extends {protorpc.Util.Error}
 */
protorpc.ValidationError = function(pattern, args) {
  this.name = 'ValidationError';
  goog.base(this, pattern, args);
};
goog.inherits(protorpc.ValidationError, protorpc.Util.Error);



/**
 * Enumeration definition error.
 * @constructor
 * @extends {protorpc.Util.Error}
 */
protorpc.EnumDefinitionError = function(pattern, args) {
  this.name = 'EnumDefinitionError';
  goog.base(this, pattern, args);
};
goog.inherits(protorpc.EnumDefinitionError, protorpc.Util.Error);



/**
 * Field definition error.
 * @constructor
 * @extends {protorpc.Util.Error}
 */
protorpc.FieldDefinitionError = function(pattern, args) {
  this.name = 'FieldDefinitionError';
  goog.base(this, pattern, args);
};
goog.inherits(protorpc.FieldDefinitionError, protorpc.Util.Error);



/**
 * Invalid variant provided to field.
 * @constructor
 * @extends {protorpc.FieldDefinitionError}
 */
protorpc.InvalidVariantError = function(pattern, args) {
  this.name = 'InvalidVariantError';
  goog.base(this, pattern, args);
};
goog.inherits(protorpc.InvalidVariantError, protorpc.FieldDefinitionError);



/**
 * Invalid number provided to field.
 * @constructor
 * @extends {protorpc.FieldDefinitionError}
 */
protorpc.InvalidNumberError = function(pattern, args) {
  this.name = 'InvalidNumberError';
  goog.base(this, pattern, args);
};
goog.inherits(protorpc.InvalidNumberError, protorpc.FieldDefinitionError);



/**
 * Duplicate number assigned to field.
 * @constructor
 * @extends {protorpc.Util.Error}
 */
protorpc.DuplicateNumberError = function(pattern, args) {
  this.name = 'DuplicateNumberError';
  goog.base(this, pattern, args);
};
goog.inherits(protorpc.DuplicateNumberError, protorpc.Util.Error);



/**
 * Raised when definition is not found.
 * @constructor
 * @extends {protorpc.Util.Error}
 */
protorpc.DefinitionNotFoundError = function(pattern, args) {
  this.name = 'DefinitionNotFoundError';
  goog.base(this, pattern, args);
};
goog.inherits(protorpc.DefinitionNotFoundError, protorpc.Util.Error);



/**
 * Error found decoding message from encoded form.
 * @constructor
 * @extends {protorpc.Util.Error}
 */
protorpc.DecodeError = function(pattern, args) {
  this.name = 'DecodeError';
  goog.base(this, pattern, args);
};
goog.inherits(protorpc.DecodeError, protorpc.Util.Error);



/**
 * @private
 * @constructor
 */
protorpc.Definition_ = function() {

};


/**
 * @private
 * @constructor
 * @extends {protorpc.Definition_}
 */
protorpc.EnumDefinition_ = function(name) {
  this.name = name;
  this.values = {};
};
goog.inherits(protorpc.EnumDefinition_, protorpc.Definition_);

/**
 * @return {protorpc.Enum_} The generated enum.
 */
protorpc.EnumDefinition_.prototype.createProxy = function() {

};

protorpc.Enum = function(name, values) {
  var def = new protorpc.EnumDefinition_(name);

  for (var number in values) {
    def[number] = 1;
  }
};


/**
 * Enumeration which allows inversing of values by name or number.
 *
 * @param {Array.<protorpc.EnumValueDescriptor>} values Enum values.
 * @constructor
 * @private
 */
protorpc.Enum_ = function(values) {
  for (var number in values) {
    var value = new protorpc.EnumValue(values[number], parseInt(number, 10));
    this[values[number]] = value;
  }
};


/**
 * Lookup value by its number.
 *
 * @param {Number} number
 * @return {protorpc.EnumValue}
 * @export
 */
protorpc.Enum_.prototype.getValueFromNumber = function(number) {
  for (var key in this) {
    if (this.hasOwnProperty(key) &&
        this[key].valueOf() == number) {
      return this[key];
    }
  }
  return null;
};



/**
 * Represents a single value in an enumeration.
 *
 * Provides an API for getting both the value as a string as well
 * as the corresponding number:
 *
 *   VALUE = protorpc.EnumValue('VALUE', 1)
 *   Number(VALUE); // returns 2
 *   String(VALUE); // returns 'VALUE'
 *
 * @param {string} name The value's name.
 * @param {Number} number the value's number.
 * @constructor
 */
protorpc.EnumValue = function(name, number) {
  /**
   * @type {string}
   * @private
   */
  this.name_ = name;

  /**
   * @type {Number}
   * @private
   */
  this.number_ = number;
};


/**
 * @inheritDoc
 */
protorpc.EnumValue.prototype.toString = function() {
  return this.name_;
};


/**
 * @return {Number}
 */
protorpc.EnumValue.prototype.valueOf = function() {
  return this.number_;
};


/**
 * Wire Variants.
 * @type {protorpc.Enum}
 */
protorpc.Variant = new protorpc.Enum({
  1: 'DOUBLE',
  2: 'FLOAT',
  3: 'INT64',
  4: 'UINT64',
  5: 'INT32',
  8: 'BOOL',
  9: 'STRING',
  11: 'MESSAGE',
  12: 'BYTES',
  13: 'UINT32',
  14: 'ENUM',
  17: 'SINT32',
  18: 'SINT64'
});



/**
 * @private
 * @constructor
 * @extends {protorpc.Definition_}
 */
protorpc.MessageDefinition_ = function(name) {
  goog.base(this);

  /**
   * @type {string}
   */
  this.name = null;

  /**
   * @type {Object.<Number, protorpc.Field>}
   */
  this.fieldMap = {};
};
goog.inherits(protorpc.MessageDefinition_, protorpc.Definition_);


/**
 * Create a getter/setter proxy this message definition.
 * @param {protorpc.Message_} instance Message instance.
 * @param {Object.<string|number, *>=} opt_defaults
 */
protorpc.MessageDefinition_.prototype.createMessageProxy = function(
  instance, opt_defaults) {
  /**
   * @constructor
   */
  var proxy = function() {
    var defaults = opt_defaults || {};

    for (var key in defaults) {
      var value = defaults[key];
      if (goog.isNumber(key)) {
        instance.setValue(key, value);
      } else if(goog.isString(key)) {
        var field = this.getFieldByName(key);
        if (field) {
          instance.setValue(field.getNumber());
        }
      }
    }
  };

  var proto = proxy.prototype;

  for (var number in this.fieldMap) {
    var field = this.fieldMap[number];
    if (field.isRepeated()) {
      var adder = protorpc.Util.toCamelCase(field.getName(), 'add');
      proto[adder] = goog.bind(instance.addValue, instance, number);
    } else {
      var setter = protorpc.Util.toCamelCase(field.getName(), 'set');
      proto[setter] = goog.bind(instance.setValue, instance, number);
    }
    var getter = protorpc.Util.toCamelCase(field.getName(), 'get');
    proto[getter] = goog.bind(instance.getValue, instance, number);
  }
  return new proxy();
};

protorpc.MessageDefinition_.prototype.getFieldByNumber = function(number) {
  return this.fieldMap[number];
};

protorpc.MessageDefinition_.prototype.getFieldByName = function(number) {
  for (var key in this.fieldMap) {
    if (this.fieldMap[key].getNumber() == number) {
      return this.fieldMap[key];
    }
  }
  return null;
};


/**
 * Message Factory.
 *
 * @param {string} name Message name.
 * @param {Object.<string, *>} options Message options.
 * @export
 */
protorpc.Message = function(name, options) {
  var definition = new protorpc.MessageDefinition_(name);
  var fields = options['fields'] || [];

  for (var fieldName in fields) {
    /** @type {protorpc.fields.Field} */
    var field = fields[fieldName](fieldName);
    definition.fieldMap[field.getNumber()] = field;
  }

  /** @constructor */
  return function(opt_defaults) {
    var instance = new protorpc.Message_(definition);
    return definition.createMessageProxy(instance, opt_defaults);
  };
};


/**
 * @private
 * @constructor
 */
protorpc.Message_ = function(definition) {
  /**
   * @type {Object.<Number, *>}
   * @private
   */
  this.values_ = {};

  /**
   * @type {protorpc.MessageDefinition_}
   * @private
   */
  this.definition_ = definition;
};

protorpc.Message_.prototype.getFieldByNumber_ = function(number) {
  return this.definition_.getFieldByNumber(number);
};

protorpc.Message_.prototype.checkInitialized = function() {
  for (var number in this.fields_) {
    var field = this.fields_[number];

    if (field.isRequired()) {
      if (goog.isNull(this.values_[number])) {
        throw new protorpc.ValidationError(
          'Message %s is missing required field %s', [
            this.getName(),
            field.getName()]);
      }
    }

    var value = this.values_[field.getNumber()];
    for (var i = 0; i < value.length; i++) {
      value[i].checkInitialized();
    }

    if (field.getVariant() == protorpc.Variant.MESSAGE) {
      this.getValue(field.getNumber()).checkInitialized();
    }

  }
};


/**
 * @return {string} The name of the Message.
 */
protorpc.Message_.prototype.getName = function() {
  return this.definition_.name;
};


/**
 * Get fields value by field's number.
 *
 * @param {number} number The field's number.
 * @return {*} The value found or undefined if no value.
 */
protorpc.Message_.prototype.getValue = function(number, opt_idx) {
  var value = this.values_[number];
  if (value) {
    return value;
  }
  var field = this.getFieldByNumber_(number);
  return field.getDefaultValue();
};


/**
 * Set the value of a field on the message.
 * @param {Number} number The field's number.
 * @param {*} value The fields value.
 */
protorpc.Message_.prototype.setValue = function(number, value) {
  this.values_[number] = value;
};


/**
 * Append an item to repeated list.
 * @param {number} number Field number.
 * @param {*} value Value to append.
 */
protorpc.Message_.prototype.addValue = function(number, value) {
  throw Error('Not Implemented');
};

